Skip to content

feat!: multi-GNSS PVT with GPS L2C/L5/L1C and Galileo E5a#48

Merged
giove-a merged 1 commit into
masterfrom
sc/multi-gnss
Jul 5, 2026
Merged

feat!: multi-GNSS PVT with GPS L2C/L5/L1C and Galileo E5a#48
giove-a merged 1 commit into
masterfrom
sc/multi-gnss

Conversation

@giove-a

@giove-a giove-a commented Jul 3, 2026

Copy link
Copy Markdown

Summary

Combine satellites from multiple GNSS in calc_pvt and add the modern GPS and Galileo civil signals (GPS L2C, L5, L1C; Galileo E5a). Breaking: PVTSolution grows new fields and the solver's minimum-satellite requirement changes.

Requires GNSSDecoder ≥ 3.3.1 and GNSSSignals ≥ 2.6.

Multi-GNSS

  • Estimate one receiver clock bias per GNSS time system; the state vector becomes [x, y, z, tc_1, …, tc_M] and a fix needs n ≥ 3 + M satellites.
  • GGTO fallback: when the independent inter-system-bias solve is under-determined but GPS is present and a Galileo satellite carries the Galileo–GPS Time Offset, collapse the Galileo clock onto GPS via the broadcast offset (range_offset = −c · GGTO), enabling a 4-satellite GPS+Galileo fix.
  • PVTSolution gains reference_system and inter_system_biases.

Inter-frequency bias (IFB)

  • When satellites span more than one frequency band, estimate one receiver IFB per band beyond a reference band — the differential RF-chain hardware delay — shared across constellations on that band (GPS L1/L1C/Galileo E1 → :L1; GPS L5/Galileo E5a → :L5; GPS L2C → :L2). State extends to [x, y, z, tc_1…tc_M, ifb_1…ifb_B]; a fix needs n ≥ 3 + M + B.
  • PVTSolution gains inter_frequency_biases (relative to the reference band; empty for single-band fixes). Velocity is unaffected (constant bias, no drift term); the IFB composes with the GGTO clock collapse.
  • Observability: the per-system clock and per-band IFB columns form the incidence matrix of the (constellation × band) coverage graph. When that graph is disconnected, those columns are collinear and the independent layout is rank-deficient; the layout now detects this and falls back (GGTO collapse when available, otherwise a single common clock/IFB parameter).

New signals

  • GPS L5 (CNAV) and L1C (CNAV-2): quasi-Keplerian ephemerides (A_REF + ΔA, Ω̇_REF + ΔΩ̇, rate terms), full-week number, ITOW/TOI time-of-week reconstruction for L1C-D.
  • GPS L2C (CNAV on the new L2 band): shares the CNAV GPSCNAVData container with L5-I — the signal is fixed by the decoder's constants type, not the data type — reusing the CNAV orbit/clock/week/Klobuchar path; ranges on the L2 CM data component with ISC_L2C.
  • Galileo E5a (F/NAV): shares the Galileo ephemeris/clock/GGTO/NTCM-G layout with E1B; applies the E1–E5a broadcast group delay.
  • Galileo E1C, GPS L1C-P, L5-Q and L2 CL pilots are not separate measurements: with joint same-band tracking the band yields one pseudorange, so pilots aid tracking upstream only.

Group-delay / inter-signal correction

Selected by the ranging signal (SatelliteState.system) with values read from the decoder's message, so a band's pseudorange may be generated on a pilot (e.g. GPS L1C-P) while the ephemeris/clock come from the data-component decoder (CNAV-2 → ISC_L1CP/ISC_L1CD/ISC_L1CA; CNAV → ISC_L5I5/ISC_L2C).

These terms are ~metre-level corrections a satellite may not have broadcast yet when its ephemeris and clock are already complete, so any of them can be nothing on a usable decoder. correct_by_group_delay therefore coalesces a missing term to zero (group_delay_term): apply each when present, treat a missing one as zero, never throw. Terms that the decoder's positioning-readiness gate guarantees (LNAV T_GD, Galileo BGD, the CNAV-2 subframe-2 terms) are always present; the ones that can genuinely lag a fix (CNAV T_GD/ISC_L2C/ISC_L5I5, CNAV-2 ISC_L1CA) are the ones this guards.

Tests

New: test/cnav.jl, test/gps_l2c.jl, test/galileo_e5a.jl, test/dop.jl, test/inter_frequency_bias.jl; expanded test/pvt.jl. Full suite green against GNSSDecoder 3.3.1.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.56522% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.27%. Comparing base (670ef06) to head (6e8f45c).

Files with missing lines Patch % Lines
src/PositionVelocityTime.jl 99.17% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #48      +/-   ##
==========================================
+ Coverage   92.77%   98.27%   +5.49%     
==========================================
  Files           7        7              
  Lines         360      464     +104     
==========================================
+ Hits          334      456     +122     
+ Misses         26        8      -18     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master 6e8f45c... master / 6e8f45c...
calc_pvt/GPSL1/4sats/cold 0.034 ± 0.0014 ms 0.0426 ± 0.0065 ms 0.797 ± 0.12
calc_pvt/GPSL1/4sats/warm 29.2 ± 3.9 μs 0.0375 ± 0.0063 ms 0.78 ± 0.17
calc_pvt/GPSL1/9sats/cold 0.0435 ± 0.00074 ms 0.0623 ± 0.0013 ms 0.698 ± 0.019
calc_pvt/GPSL1/9sats/warm 20.1 ± 0.48 μs 0.0394 ± 0.0062 ms 0.511 ± 0.081
calc_pvt/GalileoE1B/4sats/cold 0.0323 ± 0.0057 ms 0.0378 ± 0.0063 ms 0.853 ± 0.21
calc_pvt/GalileoE1B/4sats/warm 12.3 ± 0.41 μs 18.3 ± 3 μs 0.669 ± 0.11
calc_pvt/GalileoE1B/5sats/cold 0.0337 ± 0.0047 ms 0.0408 ± 0.0064 ms 0.827 ± 0.17
calc_pvt/GalileoE1B/5sats/warm 13.8 ± 0.42 μs 21.3 ± 4.2 μs 0.648 ± 0.13
time_to_load 2.26 ± 0.014 s 2.26 ± 0.0052 s 0.998 ± 0.0065
Memory benchmarks
master 6e8f45c... master / 6e8f45c...
calc_pvt/GPSL1/4sats/cold 0.312 k allocs: 16.9 kB 0.551 k allocs: 25.1 kB 0.672
calc_pvt/GPSL1/4sats/warm 0.257 k allocs: 14 kB 0.492 k allocs: 21.5 kB 0.649
calc_pvt/GPSL1/9sats/cold 0.295 k allocs: 18.3 kB 0.779 k allocs: 31.4 kB 0.581
calc_pvt/GPSL1/9sats/warm 0.141 k allocs: 9.02 kB 0.621 k allocs: 21.6 kB 0.417
calc_pvt/GalileoE1B/4sats/cold 0.301 k allocs: 16.3 kB 0.54 k allocs: 24.5 kB 0.664
calc_pvt/GalileoE1B/4sats/warm 0.136 k allocs: 7.09 kB 0.371 k allocs: 14.6 kB 0.485
calc_pvt/GalileoE1B/5sats/cold 0.291 k allocs: 16 kB 0.579 k allocs: 25.2 kB 0.634
calc_pvt/GalileoE1B/5sats/warm 0.137 k allocs: 7.34 kB 0.421 k allocs: 15.9 kB 0.462
time_to_load 0.149 k allocs: 11.2 kB 0.149 k allocs: 11.2 kB 1

@giove-a
giove-a force-pushed the sc/multi-gnss branch 2 times, most recently from f4563a3 to 6e2291a Compare July 5, 2026 16:22
Combine satellites from multiple GNSS in `calc_pvt` and add the modern
GPS and Galileo civil signals.

Multi-GNSS:
- Estimate one receiver clock bias per GNSS time system; the state vector
  becomes [x, y, z, tc_1, …, tc_M] and a fix needs n >= 3 + M satellites.
  Time-system grouping uses GNSSSignals.get_time_system(signal) and the
  GPST()/GST() TimeSystem singletons.
- GGTO fallback: when the independent inter-system-bias solve is
  under-determined but GPS is present and a Galileo satellite carries the
  Galileo-GPS Time Offset, collapse the Galileo clock onto GPS via the
  broadcast offset (range_offset = -c * GGTO), enabling a 4-satellite
  GPS+Galileo fix.
- `PVTSolution` gains `reference_system` and `inter_system_biases`, keyed by
  GNSSSignals.TimeSystem.

Inter-frequency bias:
- When satellites are processed on more than one frequency band, estimate
  one receiver inter-frequency bias (IFB) per band beyond a reference band —
  the differential RF-chain hardware delay — shared across constellations on
  that band (GPS L1/L1C/Galileo E1 -> :L1; GPS L5/Galileo E5a -> :L5;
  GPS L2C -> :L2; see `GNSSSignals.get_band_id`). The state vector extends to
  [x, y, z, tc_1…tc_M, ifb_1…ifb_B] and a fix needs n >= 3 + M + B satellites.
- `PVTSolution` gains `inter_frequency_biases` (relative to the reference
  band; empty for single-band fixes). Velocity is unaffected (a constant
  bias, no drift term); the IFB composes with the GGTO clock collapse.
- Observability: the per-system clock and per-band IFB columns form the
  incidence matrix of the (constellation × band) coverage graph. When that
  graph is disconnected — constellations occupying disjoint bands — those
  columns are collinear and the independent layout is rank-deficient, which
  previously yielded degenerate clock/IFB estimates. The layout now detects
  this and falls back: collapse the Galileo clock onto GPS via the GGTO when
  available, otherwise estimate the inter-system bias and IFB as a single
  common parameter (they can no longer be separated).

New signals:
- GPS L5 (CNAV) and L1C (CNAV-2): quasi-Keplerian ephemerides
  (A_REF + ΔA, Ω̇_REF + ΔΩ̇, rate terms) via `orbital_elements`, full-week
  number, ITOW/TOI time-of-week reconstruction for L1C-D.
- GPS L2C (CNAV on the new L2 band): shares the CNAV `GPSCNAVData` container
  with L5-I — the signal is fixed by the decoder's constants type, not the
  data type — so it reuses the CNAV orbit/clock/week/Klobuchar path; it ranges
  on the L2 CM data component with the ISC_L2C inter-signal correction, and
  introduces the L2 band into the inter-frequency-bias layout.
- Galileo E5a (F/NAV): shares the Galileo ephemeris/clock/GGTO/NTCM-G layout
  with E1B; applies the E1-E5a broadcast group delay.
- Galileo E1C, GPS L1C-P, GPS L5-Q and GPS L2 CL pilots are not separate
  measurements: with joint same-band tracking the band yields one pseudorange,
  so pilots aid tracking upstream only.

Dispatch is grouped with `GalileoNavData`, `GPSNavData` and `GPSModernNavData`
type aliases; Klobuchar is extended to all GPS civil signals and NTCM-G to
both Galileo messages.

Group delay / inter-signal correction is selected by the *ranging* signal
(`SatelliteState.system`) with values read from the decoder's message, so a
band's pseudorange may be generated on a pilot (e.g. GPS L1C-P) while the
ephemeris/clock come from the data-component decoder (CNAV-2 → ISC_L1CP vs
ISC_L1CD vs ISC_L1CA; CNAV → ISC_L5I5/ISC_L5Q5 vs the single ISC_L2C
shared by CM and CL). The transmit-time bit-count
term takes its symbol rate from GNSSSignals.get_data_frequency on the decoder
state (GNSSDecoder 3.6), not the tracked signal, so a pilot's 0 Hz data rate is
never divided by, and CNAV picks the right rate per signal (L5-I 100 Hz vs
L2C-M 50 Hz) via the decoder's constants type. Galileo's broadcast group delay
stays per band (decoder-determined).

Solver and outputs:
- `sats` is keyed by (signal, PRN) in a `Dictionary` so the same PRN can
  appear across constellations and bands; new `get_sat_info` accessor.
- `SatInfo` carries the post-fit pseudorange residual.
- DOP is rotated into the local ENU frame for HDOP/VDOP and uses a
  non-throwing Cholesky; TDOP reports the reference system's clock.
- Velocity/clock-drift solved via allocation-free 4x4 normal equations with
  a single common receiver clock drift across all systems.
- calc_pvt skips the atmospheric-delay prediction (a per-satellite geodetic +
  ENU pass) when neither ionospheric nor tropospheric correction is active, and
  builds the satellite-position matrix with `stack` in a single pass.

Type-level getters and time scale (GNSSSignals 3.3):
- The ionospheric/velocity carrier-frequency reads take constants from the
  signal *type*, never a constructed instance:
  constructing a signal builds its full code table (~120 KB GPS L1CA, ~620 KB
  Galileo E1B) and this ran once per satellite, so it cut calc_pvt allocations
  ~97% (GPS) / ~99% (Galileo).
- The time-scale origin comes from GNSSSignals.get_system_start_time (UTC),
  converted to a TAIEpoch via AstroTime's leap-aware from_utc
  (system_start_epoch); the hardcoded epochs and leap-second literals are gone.
  system_start_epoch lands on an integer TAI second for GPS/Galileo, so the
  reported-time build assumes start_time.fraction == 0 (noted inline).
- Per-signal and per-band classification uses GNSSSignals.get_signal_id
  (:GPSL1CA, …) and GNSSSignals.get_band_id (:L1, …) on each satellite's
  signal, instead of package-local helpers (GNSSSignals 3.3).
- The nav-message symbol rate for the transmit-time bit count comes from
  GNSSSignals.get_data_frequency on the decoder state (GNSSDecoder 3.6),
  replacing the six package-local nav_data_frequency methods.

Compat: GNSSDecoder 3.6, GNSSSignals 3.3, add Dictionaries, drop Statistics.

Tests: add cnav.jl, dop.jl, galileo_e5a.jl, gps_l2c.jl and
inter_frequency_bias.jl; the satellite fixtures move to test/fixtures.jl,
shared with benchmark/fixtures.jl (which keeps a GNSSDecoder major-version
shim so AirspeedVelocity can run the script against the base revision); cover combined GPS+Galileo fixes, the GGTO fallback,
(signal, PRN) keying, per-satellite residuals, ranging-signal-selected group
delay (incl. pilot-referenced ranges and ISC_L2C), per-band inter-frequency-bias
estimation (incl. GPS L1+L2), and the disjoint-band fallback.

BREAKING CHANGE: `PVTSolution.sats` is now a
`Dictionary{Tuple{Symbol,Int},SatInfo}` (was `Dict{Int,SatInfo}`); index it
with `get_sat_info(pvt, signal, prn)`. `SatInfo` gains a `residual` field and
`calc_DOP` takes the user position and primary clock index. The
`get_gdop`/`get_pdop`/`get_hdop`/`get_vdop`/`get_tdop` accessors are removed;
read DOP from the `dop` field instead (e.g. `pvt.dop.GDOP`).
`get_num_used_sats` is removed: with `sats` keyed by (signal, PRN),
`length(pvt.sats)` counts measurements, not satellites.
`get_frequency_offset` is removed: compute it inline as
`pvt.relative_clock_drift * base_frequency`.
`PVTSolution.reference_system` and the `inter_system_biases` keys are now
`GNSSSignals.TimeSystem` values (GPST()/GST()), not :GPS/:Galileo symbols.
Requires GNSSSignals 3.3 and GNSSDecoder 3.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@giove-a
giove-a merged commit a968fff into master Jul 5, 2026
8 checks passed
@giove-a
giove-a deleted the sc/multi-gnss branch July 5, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants